/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import commonEvent from '@ohos.commonEventManager';
import {DownloadTask} from './DownloadTask'
import {StartEndListener} from './listener/StartEndListener'
import {DownloadContextListener} from './DownloadContextListener'
import {EndCause} from './cause/EndCause'
import {DownloadListener} from './DownloadListener'
import {DownloadListenerBunch} from './listener/DownloadListenerBunch'
import {OkDownload} from './OkDownload'
import {Builder} from './Builder'
import {Log} from './Util'
import { LogUtil } from './LogUtil';
const TAG = 'DownloadContext'

export class DownloadContextBuilder{
  boundTaskList: Array<DownloadTask>;
  set: QueueSet;
  listener: DownloadContextListener;

  public constructor(set: QueueSet = new QueueSet(), taskArrayList: Array<DownloadTask> = new Array) {
    LogUtil.log('DownloadContext constructor');
    this.set = set;
    this.boundTaskList = taskArrayList;
  }

  public setListener(listener: DownloadContextListener) {
    LogUtil.log('DownloadContext setListener');
    this.listener = listener;
    return this;
  }

  public bindSetTask(task: DownloadTask) {
    LogUtil.log('DownloadContext bindSetTask');
    const index: number = this.boundTaskList.indexOf(task);
    if (index >= 0) {
      // replace
      this.boundTaskList.splice(index, 1, task);
    } else {
      this.boundTaskList.push(task);
    }
    return this;
  }

  public bindUrl(url: string, filename: string): DownloadTask {
    LogUtil.log('DownloadContext bindUrl');
    if (this.set.uri == null) {
      throw new Error("If you want to bind only with url, you have to"
        + " provide parentPath on QueueSet!");
    }
    return this.bind(url, filename, this.set.uri);
  }

  public bind(url: string, filename: string, uri: string): DownloadTask {
    LogUtil.log('DownloadContext bind');
    // const build = new Builder(url, filename, uri)
    let taskBuilder = new Builder(url, filename, uri);
    if (this.set.headerMapFields != null) taskBuilder.setHeaderMapFields(this.set.headerMapFields);
    if (this.set.wifiRequired != null) taskBuilder.setWifiRequired(this.set.wifiRequired);

    if (this.set.passIfAlreadyCompleted != null) {
      taskBuilder.setPassIfAlreadyCompleted(this.set.passIfAlreadyCompleted);
    }

    const task: DownloadTask = taskBuilder.build();
    if (this.set.tag != null) task.setTag(this.set.tag);

    this.boundTaskList.push(task);
    return task;
  }

  public unbindTask(task: DownloadTask): void {
    LogUtil.log('DownloadContext unbindTask');
    let index = this.boundTaskList.indexOf(task)
    this.boundTaskList.splice(index, 1);
  }

  public unbind(id: number): void {
    LogUtil.log('DownloadContext unbind');
    //let list: Array<DownloadTask> = Object.assign(this.boundTaskList);
    for (let task of this.boundTaskList) {
      if (task.getId() === id) {
        let index = this.boundTaskList.indexOf(task);
        this.boundTaskList.splice(index, 1)
      }
    }
  }

  public build(): DownloadContext {
    LogUtil.log('DownloadContext build');
    let tasks: DownloadTask[] = this.boundTaskList;
    console.info("okdownload tasks = " + tasks.length)
    return new DownloadContext(tasks, this.listener, this.set);
  }
}

export class DownloadContext {
  tasks: DownloadTask[];
  started: boolean = false;
  contextListener: DownloadContextListener;
  set: QueueSet;
  private completeSubscriber: any;

  constructor(tasks: DownloadTask[], contextListener: DownloadContextListener, set: QueueSet) {
    this.tasks = tasks;
    this.contextListener = contextListener;
    this.set = set;
  }

  public isStarted(): boolean {
    LogUtil.log('DownloadContext isStarted');
    return this.started;
  }

  public getTasks(): DownloadTask[] {
    LogUtil.log('DownloadContext getTasks');
    return this.tasks;
  }

  public startOnSerial(listener: DownloadListener): void {
    LogUtil.log('DownloadContext startOnSerial');
    this.start(listener, true);
  }

  public startOnParallel(listener: DownloadListener): void {
    LogUtil.log('DownloadContext startOnParallel');
    this.start(listener, false);
  }

  /**
   * Start queue.
   *
   * @param listener the listener for each task, if you have already provided
   *                {@link #contextListener}, it's accept {@code null} for each task's listener.
   * @param isSerial whether download queue serial or parallel.
   */
  public async start(listener: DownloadListener, isSerial: boolean) {
    LogUtil.log('DownloadContext start');
    try {
      const startTime: number = new Date().getTime();
      LogUtil.log(`start ${isSerial}`);
      this.started = true;
      let targetListener: DownloadListener;
      if (this.contextListener != null) {
        targetListener = new DownloadListenerBunch.Builder()
          .append(listener)
          .append(new QueueAttachListener(this, this.contextListener, this.tasks.length))
          .build();
      } else {
        targetListener = listener;
      }


      if (isSerial) {
        let tempTasks = Array.from(this.tasks)
        tempTasks.sort((a: DownloadTask, b: DownloadTask) => {
          return b.getPriority() - a.getPriority();
        });
        tempTasks[0].execute(targetListener);

        let num: number = 0

        let completeInfo = {events: ['completed']};
        this.completeSubscriber = await commonEvent.createSubscriber(completeInfo);
        commonEvent.subscribe(this.completeSubscriber, (error, commonEventData) => {
          if (error) {
            LogUtil.error(`get completed failed. Cause: ${JSON.stringify(error)}`);
          } else {
            let temp = ++num;
            LogUtil.log(`execute task : ${temp}`);
            if(temp < this.tasks.length) {
              tempTasks[temp].execute(targetListener);
            } else {
              num = 0;
              commonEvent.unsubscribe(this.completeSubscriber, (error, commonEventData) => {
              });
            }
          }
        });
      } else {
        DownloadTask.enqueueTasks(this.tasks, targetListener);
      }
      LogUtil.log(`start finish ${isSerial} ${(new Date().getTime() - startTime)}ms`);
    } catch (e) {
      LogUtil.error(`DownloadContext start err: ${JSON.stringify(e)}`);
    }
  }

  public alter(): AlterContext {
    LogUtil.log('DownloadContext alter');
    return new AlterContext(this);
  }

  public async stop() {
    LogUtil.log('DownloadContext stop');
    try {
      if (this.completeSubscriber != null) {
        await commonEvent.unsubscribe(this.completeSubscriber, (error, commonEventData) => {
          this.completeSubscriber = null;
        });
      }
      if (this.started) OkDownload.with().getDownloadDispatcher().cancelTasks(this.tasks);
      this.started = false;
    } catch (e) {
      LogUtil.error(`DownloadContext stop err: ${JSON.stringify(e)}`);
    }
  }

  private callbackQueueEndOnSerialLoop(): void {
    if (this.contextListener == null) return;
    this.contextListener.queueEnd(this);
  }

  public toBuilder() {
    LogUtil.log('DownloadContext toBuilder');
    return new DownloadContext.Builder(this.set, this.tasks)
      .setListener(this.contextListener);
  }

  public static Builder = DownloadContextBuilder;
}

/**
 * The Alter helper for the {@link DownloadContext}.
 */
export class AlterContext {
  private context: DownloadContext;

  constructor(context: DownloadContext) {
    this.context = context;
  }

  /**
   * Replace the {@code oldTask} to the {@code newTask}
   *
   * @param oldTask the old task which has been added to the context.
   * @param newTask the new task which will be replace the {@code oldTask} on the
   *                {@code context}.
   */
  public replaceTask(oldTask: DownloadTask, newTask: DownloadTask) {
    LogUtil.log('DownloadContext replaceTask');
    const tasks: DownloadTask[] = this.context.tasks;
    for (var i = 0; i < tasks.length; i++) {
      const task: DownloadTask = tasks[i];
      if (task === oldTask) {
        tasks[i] = newTask;
      }
    }

    return this;
  }
}

export class QueueSet {
  headerMapFields: object;
  uri: string;
  passIfAlreadyCompleted: boolean;
  wifiRequired: boolean;
  tag: object;

  public getHeaderMapFields(): object{
    LogUtil.log('DownloadContext getHeaderMapFields');
    return this.headerMapFields;
  }

  public setHeaderMapFields(headerMapFields: object): void {
    LogUtil.log('DownloadContext setHeaderMapFields');
    this.headerMapFields = headerMapFields;
  }

  public getDirUri(): string {
    LogUtil.log('DownloadContext getDirUri');
    return this.uri;
  }

  public setParentPath(uri: string) {
    LogUtil.log('DownloadContext setParentPath');
    this.uri = uri;
    return this;
  }

  public setWifiRequired(wifiRequired: boolean) {
    LogUtil.log('DownloadContext setWifiRequired');
    this.wifiRequired = wifiRequired;
    return this;
  }

  public isWifiRequired(): boolean {
    LogUtil.log('DownloadContext isWifiRequired');
    return this.wifiRequired == null
      ?Builder.DEFAULT_IS_WIFI_REQUIRED : this.wifiRequired;
  }

  public getTag(): object {
    LogUtil.log('DownloadContext getTag');
    return this.tag;
  }

  public setTag(tag: object) {
    LogUtil.log('DownloadContext setTag');
    this.tag = tag;
    return this;
  }

  public isPassIfAlreadyCompleted(): boolean {
    LogUtil.log('DownloadContext isPassIfAlreadyCompleted');
    return this.passIfAlreadyCompleted == null
      ? Builder.DEFAULT_PASS_IF_ALREADY_COMPLETED
      : this.passIfAlreadyCompleted;
  }

  public setPassIfAlreadyCompleted(passIfAlreadyCompleted: boolean) {
    LogUtil.log('DownloadContext setPassIfAlreadyCompleted');
    this.passIfAlreadyCompleted = passIfAlreadyCompleted;
    return this;
  }

  public commit() {
    LogUtil.log('DownloadContext commit');
    return new DownloadContext.Builder(this);
  }
}

class QueueAttachListener extends StartEndListener {
  private remainCount: number;
  private contextListener: DownloadContextListener;
  private hostContext: DownloadContext;

  constructor(context: DownloadContext, contextListener: DownloadContextListener, taskCount: number) {
    super();
    this.remainCount = taskCount;
    this.contextListener = contextListener;
    this.hostContext = context;
  }

  public taskStart(task: DownloadTask): void {
    LogUtil.log('DownloadContext taskStart');
  }

  public taskEnd(task: DownloadTask, cause: EndCause, realCause: Error): void {
    LogUtil.log('DownloadContext taskEnd');
    this.remainCount--;
    this.contextListener.taskEnd(this.hostContext, task, cause, realCause, this.remainCount);
    if (this.remainCount <= 0) {
      this.contextListener.queueEnd(this.hostContext);
      // only log the last one
      LogUtil.log(`taskEnd and remainCount ${this.remainCount}`);
    }
  }
}